package shovelsoft.btmetaparser;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import shovelsoft.btmetaparser.structs.INode;
import shovelsoft.btmetaparser.tokens.Data;
import shovelsoft.btmetaparser.tokens.Dictionary;
import shovelsoft.btmetaparser.tokens.IToken;

/***
 * Parse a bittorrent metafile.
 * @author cds
 */
public class Metafile {
	
	/***
	 * Test program. Create and print an instance of itself.
	 * @param args Filename to open
	 */
	public static void main(String[] args) {
		
		Metafile mf;
		String file;
		
		if(args.length == 0) {
			// file = "c:\\test.torrent"; // windows
			//file = "/home/cds/Downloads/test.torrent"; // linux - single file torrent
			file = "/home/cds/Downloads/collection_of_linux_distros.5883961.TPB.torrent"; // linux - multi-file torrent
		}
		else
			file = args[0];
		
		System.out.println("opening " + file);
		
		InputStream is;
		try {			
			is = new FileInputStream(file);
			mf = new Metafile(is);
			System.out.println(mf);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	
	public class Files {
		
		public final Long 			length;
		public final String 		md5sum;
		public final List<String> 	path;
		
		public Files(Long length, String md5sum, List<String> path) {
			
			this.length = length;
			this.md5sum = md5sum;
			this.path   = path;
		}
		
		public String toString() {
		
			StringBuilder sb = new StringBuilder();
			
			sb.append("\n  length: " + length);
			sb.append("\n  md5sum: " + md5sum);
			sb.append("\n  path: ");
			for(String s : path)
				sb.append(s + "\n");
			
			return sb.toString();
		}
	}
	
	final boolean					multiFileMode;
	
	// global
	final String 					announce;			// mandatory
	final List<List<String> > 		announceList;		// optional
	final Long						creationEpoch;		// optional
	final String					comment;			// optional
	final String					createdBy;			// optional
	final String					encoding;			// optional
	
	
	// from info dictionary
	final Long						piece_length;		// mandatory
	final String					pieces;				// mandatory
	final Long						_private;			// optional
	final String					name;				// always mandatory
	final Long						length;				// mandatory in single-file mode
	final String					md5sum;				// always optional
	final List<Files>				files;				// mandatory in multi-file mode

	public Metafile(InputStream is) throws Exception {
		
		Parse p = new Parse(is);
		
	
		
		INode global = p.getRoot();
		
		/*** GLOBAL FIELDS ***/
		announce 		= parseStringInDict(global, "announce", 	false);
		comment			= parseStringInDict(global, "comment", 		true);
		createdBy		= parseStringInDict(global, "created by",	true);
		encoding		= parseStringInDict(global, "encoding", 	true);
		announceList	= parseAnnounceList(global);
		creationEpoch	= parseIntegerInDict(global, "creation date", true);
		
		/*** INFO FIELDS ***/
		INode info		= resolveInDictionary(global, "info");
		if(info == null)
			throw new Exception("parse error: missing 'info' dictionary");
	
		piece_length	= parseIntegerInDict (info, "piece length", false);
		pieces			= parseStringInDict  (info, "pieces",       false);
		_private		= parseIntegerInDict (info, "private",  	true);
		name 			= parseStringInDict	 (info, "name", 		false);
		
		/*** TODO ***/
		files 			= parseFiles(info);
		multiFileMode = (files != null);
		
		length = parseIntegerInDict(info,"length", multiFileMode); 
		md5sum = parseStringInDict(info,"md5sum", true);
		
	}

	@Override
	public String toString() {
		
		StringBuilder sb = new StringBuilder();
		
		sb.append( ts("announce", announce));
		
		if(announceList != null) {
			sb.append("announce-list:\n");
			for(List<String> l : announceList) {
				for(String s : l) {
					sb.append( "  " + s + "\n");
				}
			}
		}
		else
			sb.append("announce-list: null\n");
		
		
		sb.append( ts("creationEpoch", creationEpoch));
		sb.append( ts("comment", comment));
		sb.append( ts("createdBy", createdBy));
		sb.append( ts("encoding", encoding));
		sb.append( ts("piece_length", piece_length));
		if(pieces!=null)
			sb.append("pieces: <binary data>\n");
		else
			sb.append("pieces: null\n");
		sb.append( ts("private", _private));
		sb.append( ts("name", name));
		sb.append( ts("length", length));
		sb.append( ts("md5sum", md5sum));
		
		if(files != null) {
			sb.append("files:\n");
			for(Files f : files)
				sb.append(f);
		}
		else
			sb.append( ts("files", null));
			
		
		return sb.toString();
	}
	
	private String ts(String name,Object Obj) {
		
		if(Obj != null)
			return (name + ": " + Obj + "\n");
		else
			return (name + ": null\n");
	}
	
	private List<Files> parseFiles(INode node) throws Exception {
		
		INode files = resolveInDictionary(node,"files");
		
		if(files == null)
			return null;
		
		List<INode> nodeList  = toList(files);
		List<Files> filesList = new LinkedList<Files>();
		
		for(INode file : nodeList)
			filesList.add( newFile(file) );
		
		return filesList;
	}
	
	private Files newFile(INode dict) throws Exception {
		
		Long length = parseIntegerInDict(dict, "length", false);
		String md5sum = parseStringInDict(dict, "md5sum", true);
		
		List<INode> pathNodeList = toList( resolveInDictionary(dict, "path"));
		
		if(pathNodeList == null)
			throw new Exception("parse error: expected 'path' in info.files[]");
		
		List<String> pathList = new LinkedList<String>();
		
		for(INode n : pathNodeList) {
			if(!(n.getToken() instanceof shovelsoft.btmetaparser.tokens.Data))
				throw new Exception("parse error: expected data type in info.files[][]");
			pathList.add(new String(((shovelsoft.btmetaparser.tokens.Data)n.getToken()).mData));
		}

		return new Files(length, md5sum, pathList);
	}
	
	/***
	 * Resolve a string associated with a key in a dictionary struct.
	 * @param dict a dictionary node
	 * @param key dictionary key
	 * @param optional Throw an exception if key is not found and optional is false.
	 * @return string associated with given string.
	 * @throws Exception
	 */
	private String parseStringInDict(INode dict, String key, boolean optional) throws Exception {

		INode strNode = resolveInDictionary(dict, key);
		
		if(strNode == null) {
			if(optional)
				return null;
			
			StringBuilder sb = new StringBuilder();
			int idx=0;
			for(Iterator<INode> it = dict.getChildren().iterator(); it.hasNext(); )
				sb.append(" " + (idx++) + ")" + it.next().getToken() + "\n");
			
			throw new Exception("parse error: expected '" +  key + "' in dictionary\n" + sb);
		}
		
		if(!(strNode.getToken() instanceof shovelsoft.btmetaparser.tokens.Data))
			throw new Exception("parse error: expected Data type in '" + key + "'");
			
		return new String(((shovelsoft.btmetaparser.tokens.Data)strNode.getToken()).mData);
	}

	/***
	 * Resolve an integer associated with a key in a dictionary struct.
	 * @param dict a dictionary node
	 * @param key dictionary key
	 * @param optional Throw an exception if key is not found and optional is false.
	 * @return integer associated with given string.
	 * @throws Exception
	 */
	private Long parseIntegerInDict(INode dict, String key, boolean optional) throws Exception {

		INode creationNode = resolveInDictionary(dict, key);
		
		if(creationNode == null) {
			if(optional)
				return null;
			
			throw new Exception("parse error: expected '" +  key + "'");
		}
		
		if(!(creationNode.getToken() instanceof shovelsoft.btmetaparser.tokens.Integer))
			throw new Exception("parse error: expected Integer type in '" + key + "'");
			
		return ((shovelsoft.btmetaparser.tokens.Integer)creationNode.getToken()).mLong;
	}
	
	/***
	 * Convert the announce-list tree into a list of lists of tracker URL's
	 * @param global The root of the metafile tree.
	 * @return list of list of tracker URL's
	 * @throws Exception
	 */
	private List<List<String> > parseAnnounceList(INode global) throws Exception {
		
		INode root = resolveInDictionary(global, "announce-list");
		if(root == null)
			return null;
		
		if(!(root.getToken() instanceof shovelsoft.btmetaparser.tokens.List))
			throw new Exception("parse error: expected list type in 'announce-list'");
		
		List<List<String>> ann = new LinkedList<List<String>>();
		
		int n=0;
		
		for(INode list : root.getChildren() ) {
			
			if(!(list.getToken() instanceof shovelsoft.btmetaparser.tokens.List))
				throw new Exception("parse error: expected list type in 'announce-list.list[" + n + "]'");
			
			List<String> ann2 = new LinkedList<String>();
			ann.add( ann2 );
			
			int m=0;
			for(INode str : list.getChildren() ) {
				
				if(!(str.getToken() instanceof shovelsoft.btmetaparser.tokens.Data))
					throw new Exception("parse error: expected Data type in 'announce-list.list[" + n + "][" + m + "]'");
			
				ann2.add(new String(((Data)(str.getToken())).mData));
				
				++m;
			}
			
			++n;
		}
		return ann;
	}
	
	/***
	 * Assert that given node is of the correct type (List Token), and return its children.
	 * @param list
	 * @return Return the given lists child nodes.
	 * @throws Exception
	 */
	private List<INode> toList(INode list) throws Exception {
		
		if(list == null)
			throw new Exception("parse error: expected a list struct");
		if(!(list.getToken() instanceof shovelsoft.btmetaparser.tokens.List))
			throw new Exception("parse error: expected a list struct");
				
		return list.getChildren();
	}
	
	/***
	 * Find searchKey in a dictionary, and return the Node it is mapped to.
	 * @param node - Dictionary to search
	 * @param searchKey - String key to search for.
	 * @return The INode that the given searchKey was mapped to, or null.
	 * @throws Exception 
	 */
	private INode resolveInDictionary(INode node, String searchKey) throws Exception {
		
		return resolveInDictionary(node, new Data(searchKey.getBytes()));
	}
	
	/***
	 * Find searchKey in a dictionary, and return the Node it is mapped to.
	 * @param node - Dictionary to search
	 * @param searchKey - Token key to search for.
	 * @return The INode that the given searchKey was mapped to, or null.
	 * @throws Exception 
	 */
	private INode resolveInDictionary(INode node, IToken searchKey) throws Exception {
		
		if(node == null)
			return null;
		
		IToken token = node.getToken();
		
		if(!(token instanceof Dictionary))
			return null;
		
		if((node.getChildren().size() & 1) != 0)
		{
			StringBuilder sb = new StringBuilder();
			int idx=0;
			for(Iterator<INode> it = node.getChildren().iterator(); it.hasNext(); )
				sb.append(" " + (idx++) + ")" + it.next().getToken() + "\n");
			throw new Exception("parse error: mal-formed dictionary" + "\n" + sb);
		}
		
		for(Iterator<INode> it = node.getChildren().iterator(); it.hasNext(); ) {
			
			IToken key    = it.next().getToken();
			INode element = it.next();
			
			if(key.equals(searchKey))
				return element;
		}
		return null;
	}
}


